-- Copyright (c) 2024 Huawei Technologies Co., Ltd.
-- openUBMC is licensed under Mulan PSL v2.
-- You can use this software according to the terms and conditions of the Mulan PSL v2.
-- You may obtain a copy of Mulan PSL v2 at: http://license.coscl.org.cn/MulanPSL2
-- THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
-- EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
-- MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
-- See the Mulan PSL v2 for more details.
local log = require 'mc.logging'
local skynet = require 'skynet'
local c_object = require 'mc.orm.object'
local c_object_manage = require 'mc.orm.object_manage'
local ctx = require 'mc.context'
local client = require 'power_mgmt.client'
local psu_def = require 'macros.psu_def'
local utils = require 'power_mgmt_utils'
local c_psu_slot_object = require 'device.psu_slot'
local c_efficiency_curve_object = require 'efficiency_curve'
local system_power = require 'system_power'
local fw_def = require 'macros.fw_def'
local mdb = require 'mc.mdb'
local power_mgmt = require 'device.power_mgmt'
local custom_msg = require 'messages.custom'
local FWINVENTORY_PATH = '/bmc/kepler/UpdateService/FirmwareInventory/'
local FWINFO_INTERFACE = 'bmc.kepler.UpdateService.FirmwareInfo'
local enums = require 'macros.power_mgmt_enums'
local base_msg = require 'messages.base'
local log_def = require 'macros.log_def'
local utils_core = require 'utils.core'

local E_OK <const> = nil -- 函数执行成功返回nil
local E_FAILED <const> = '' -- 空错误信息

local INPUT_ERROR_BIT <const> = 6
local DEFAULT <const> = -1

local psu_manager = c_object('OnePower')

function psu_manager:ctor()
    self.ps_id                 = 0
    self.psu_slot              = c_psu_slot_object.collection:find({SlotNumber = self.SlotNumber})
    self.firmware_id           = ''
    self.system_power_instance = system_power.get_instance()
    self.power_mgmt_instance   = power_mgmt.get_instance()
    self.last_comm_status      = 0
    self.last_firmware_version = ''
    self.last_rate             = DEFAULT
    self.last_input_voltage    = DEFAULT
    self.last_collect_time = 0
    self.last_tspu_status_high = 0
    self.last_tspu_status_low = 0
end

function psu_manager:dtor()
    self:is_mismatch()
    self:unmount_fw_info()
end

function psu_manager:unmount_fw_info()
    local ok, err = client:FirmwareInventoryFirmwareInventoryDelete(
        ctx.new(),
        self.firmware_id
    )
    if not ok then
        log:error('unmount PSU%d firmware failed, err = %s', self.ps_id, err)
    else
        log:notice('unmount PSU%d firmware successfully', self.ps_id)
    end
end

function psu_manager:register_fw_info()
    -- 前向兼容，电源砖固件资源路径与普通电源保持一致
    local device_locator = string.gsub(self.DeviceLocator, "PowerConverter", "PSU")
    self.firmware_id = string.format('%s_%s', self.Position, device_locator)
    local ok, err = client:PFirmwareInventoryFirmwareInventoryAdd(
        ctx.new(),
        {
            Id = self.firmware_id,
            Name = self.DeviceLocator, Version = self.FirmwareVersion or '',
            BuildNum = '', ReleaseDate = '', LowestSupportedVersion = '',
            SoftwareId = 'PSU-' .. self.Model, Manufacturer = self.Manufacturer,
            Location = self.Position, State = 'Enabled', Severity = 'Informational'
        },
        true, 1, 10 -- 电源固件包当前最大10MB
    )
    if not ok then
        log:error('register PSU%s firmware failed, err = %s', self.ps_id, err)
    else
        log:notice('register PSU%s firmware successfully', self.ps_id)
    end
end

function psu_manager:update_firmware_info()
    local path = FWINVENTORY_PATH .. self.firmware_id
    local bus = c_object_manage.get_instance().bus
    local ok, obj = pcall(mdb.get_cached_object, bus, path, FWINFO_INTERFACE)
    if not ok then
        log:error('[PowerUpgrade] get firmware object fail, path: %s, error: %s', path, obj)
        return
    end
    if obj.Version ~= self.FirmwareVersion then
        obj.Version = self.FirmwareVersion
        log:notice('[PowerUpgrade] updated firmware version %s to FirmwareInventory, id: %s',
            obj.Version, self.firmware_id)
    end
end

function psu_manager:get_life_prediction_level()
    local ok, err_info, level = pcall(function ()
        return self.Power.methods.GetLifePredictionLevel()
    end)
    if not ok or err_info then
        log:error('get ps%s life prediction level failed, error: %s', self.ps_id, err_info)
        return E_FAILED
    end
    return E_OK, level
end

function psu_manager:get_life_prediction_count()
    local ok, err_info, index, count = pcall(function ()
        return self.Power.methods.GetLifePredictionCount()
    end)
    if not ok or err_info then
        log:error('get ps%s life prediction count failed, error: %s', self.ps_id, err_info)
        return E_FAILED
    end
    return E_OK, index, count
end

function psu_manager:set_life_prediction_count_index(dod_level, vout_level, current_level)
    local ok, err_info = pcall(function ()
        return self.Power.methods.SetLifePredictionCountIndex(dod_level, vout_level, current_level)
    end)
    if not ok or err_info then
        log:error('set ps%s life prediction count index failed, error: %s', self.ps_id, err_info)
        return E_FAILED
    end
    return E_OK
end

function psu_manager:get_black_box_data()
    local ok, black_box_max_length, black_box_data = pcall(function ()
        return self.BlackBox.methods.GetBlackBoxData()
    end)
    if not ok then
        log:error('get_black_box_data failed, ps%s', self.ps_id)
        return E_FAILED, black_box_max_length
    end
    return E_OK, black_box_max_length, black_box_data
end

function psu_manager:get_main_circuit_input_voltage_type()
    local ok, err_info, resp = pcall(function ()
        return self.Power.methods.GetMainCircuitInputVoltageType()
    end)
    if not ok or err_info then
        log:error('get ps%s main circuit input voltage type failed, error: %s', self.ps_id, err_info)
        return E_FAILED
    end
    return E_OK, resp
end

function psu_manager:get_backup_circuit_input_voltage_type()
    local ok, err_info, resp = pcall(function ()
        return self.Power.methods.GetBackupCircuitInputVoltageType()
    end)
    if not ok or err_info then
        log:error('get ps%s backup circuit input voltage type failed, error: %s', self.ps_id, err_info)
        return E_FAILED
    end
    return E_OK, resp
end

function psu_manager:get_main_circuit_input_voltage()
    local ok, err_info, resp = pcall(function ()
        return self.Power.methods.GetMainCircuitInputVoltage()
    end)
    if not ok or err_info then
        log:error('get ps%s main circuit input voltage failed, error: %s', self.ps_id, err_info)
        return E_FAILED
    end
    return E_OK, resp
end

function psu_manager:get_backup_circuit_input_voltage()
    local ok, err_info, resp = pcall(function ()
        return self.Power.methods.GetBackupCircuitInputVoltage()
    end)
    if not ok or err_info then
        log:error('get ps%s backup circuit input voltage failed, error: %s', self.ps_id, err_info)
        return E_FAILED
    end
    return E_OK, resp
end

function psu_manager:set_work_mode(work_mode)
    local ok, resp = pcall(function ()
        self.PowerMode.methods.SetWorkMode(work_mode)
    end)
    if not ok then
        log:error('set ps%u work_mode to %s failed: %s', self.ps_id, work_mode, resp)
        return E_FAILED
    end
    return E_OK
end

function psu_manager:set_sleep_mode(sleep_mode)
    local ok, resp = pcall(function ()
        return self.PowerMode.methods.SetSleepMode(sleep_mode)
    end)
    if not ok then
        log:error('set ps%u sleep_mode to %s failed: %s', self.ps_id, sleep_mode, resp)
        if resp.name == base_msg.ActionNotSupportedMessage.Name then
            return enums.SLEEP_MODE_RELATED_ERROR.ACTION_NOT_SUPPORTED
        else
            return enums.SLEEP_MODE_RELATED_ERROR.OPERATION_FAILED
        end
    end
    return E_OK
end

function psu_manager:reset(reset_type)
    local ok, error_info = pcall(function ()
        return self.Power.methods.Reset(reset_type)
    end)
    if not ok or error_info then
        return error_info
    end
    return E_OK
end

function psu_manager:set_psu_fan_rpm(rpm)
    local ok, resp = pcall(function ()
        self.Cooling.methods.SetFanRPM(rpm)
    end)
    if not ok then
        log:error('set ps%s psu_fan_rpm to %s failed, error: %s', self.ps_id, rpm, resp)
        return E_FAILED
    end
    return E_OK
end

function psu_manager:set_psu_power_latch()
    local ok, resp = pcall(function ()
        if not self.Power.methods.PowerLatch then
            log:notice('ps%s not support PowerLatch', self.ps_id)
            return E_FAILED
        end
        return self.Power.methods.PowerLatch()
    end)
    if not ok then
        log:error('set ps%s set_psu_power_latch failed, error: %s', self.ps_id, resp)
        return E_FAILED
    end
    return resp
end

function psu_manager:set_alarm_latch(switch_status)
    local ok, error_info = pcall(function ()
        return self.Power.methods.SetAlarmLatch(switch_status)
    end)
    if not ok or error_info then
        return error_info
    end
    return E_OK
end

function psu_manager:set_power_supply_circuit(circuit)
    local ok, error_info = pcall(function ()
        return self.Power.methods.SetPowerSupplyCircuit(circuit)
    end)
    if not ok or error_info then
        return error_info
    end
    return E_OK
end

function psu_manager:set_output_limit_point_watts(output_power_limit_watts)
    local ok, error_info = pcall(function ()
        return self.Power.methods.SetOutputPowerLimitWatts(output_power_limit_watts)
    end)
    if not ok or error_info then
        return error_info
    end
    return E_OK
end

function psu_manager:set_retransfer_delay_seconds(second)
    local ok, resp = pcall(function ()
        return self.Power.methods.SetRetransferDelaySeconds(second)
    end)
    if not ok then
        log:error('set_retransfer_delay_seconds %s failed: %s', self.ps_id, resp)
        return E_FAILED
    end
    return E_OK
end

function psu_manager:get_retransfer_delay_seconds()
    local ok, resp = pcall(function ()
        return self.Power.methods.GetRetransferDelaySeconds()
    end)
    if not ok then
        log:error('get_retransfer_delay_seconds failed, ps%s', self.ps_id)
        error(custom_msg.OperationFailed())
    end
    return resp
end

function psu_manager:set_vout_debounce_milliseconds(debounce_milliseconds)
    local ok, error_info = pcall(function ()
        return self.Power.methods.SetVOUTDebounceMilliseconds(debounce_milliseconds)
    end)
    if not ok or error_info then
        return error_info
    end
    return E_OK
end

function psu_manager:set_power_capacitor_calibrate()
    local ok, error_info = pcall(function ()
        return self.Power.methods.SetPowerCapacitorCalibrate()
    end)
    if not ok or error_info then
        return error_info
    end
    return E_OK
end

function psu_manager:get_power_capacitor_calibrate_time()
    local ok, resp = pcall(function ()
        return self.Power.methods.GetPowerCapacitorCalibrateTime()
    end)
    if not ok then
        return resp
    end
    return resp
end

function psu_manager:get_power_capacitor_calibrate()
    local ok, error_info = pcall(function ()
        return self.Power.methods.GetPowerCapacitorCalibrate()
    end)
    if not ok or error_info then
        return error_info
    end
    return E_OK
end

function psu_manager:set_depth_of_discharge_volts(depth_of_discharge_volts)
    local ok, error_info = pcall(function ()
        return self.Power.methods.SetDepthOfDischargeVolts(depth_of_discharge_volts)
    end)
    if not ok or error_info then
        return error_info
    end
    return E_OK
end

function psu_manager:set_power_capacitor_enable(enable)
    local ok, resp = pcall(function ()
        if not self.Power.methods.SetPowerCapacitorEnable then
            log:error('ps%s not support Set PowerCapacitorEnable', self.ps_id)
            return E_FAILED
        end
        return self.Power.methods.SetPowerCapacitorEnable(enable)
    end)
    if not ok then
        resp = E_FAILED
        log:error('set ps%d capacitor enable failed', self.ps_id, resp)
    end
    return resp
end

function psu_manager:get_upgrade_channel()
    local ok, resp = pcall(function ()
        if not self.Upgrade.methods.GetUpgradeChannel then
            log:notice('ps%s not support GetUpgradeChannel', self.ps_id)
            return E_FAILED
        end
        return self.Upgrade.methods.GetUpgradeChannel()
    end)
    if not ok then
        resp = E_FAILED
        log:error('set ps%s get_upgrade_channel failed, error: %s', self.ps_id, resp)
    end
    return resp
end

function psu_manager:change_polling_channel(upgrade_channel)
    local ok, resp = pcall(function ()
        if not self.Upgrade.methods.ChangePollingChannel then
            log:notice('ps%s not support ChangePollingChannel', self.ps_id)
            return E_FAILED
        end
        return self.Upgrade.methods.ChangePollingChannel(upgrade_channel)
    end)
    if not ok then
        resp = E_FAILED
        log:error('set ps%s change_polling_channel failed, error: %s', self.ps_id, resp)
    end
    return resp
end

function psu_manager:recover_polling_channel()
    local ok, resp = pcall(function ()
        if not self.Upgrade.methods.RecoverPollingChannel then
            log:notice('ps%s not support RecoverPollingChannel', self.ps_id)
            return E_FAILED
        end
        return self.Upgrade.methods.RecoverPollingChannel()
    end)
    if not ok then
        resp = E_FAILED
        log:error('set ps%s recover_polling_channel failed, error: %s', self.ps_id, resp)
    end
    return resp
end

function psu_manager:get_psu_fan_rpm()
    local ok, resp = pcall(function ()
        return self.Cooling.methods.GetFanRPM()
    end)
    if not ok then
        log:error('get ps%u psu_fan_rpm failed, error: %s', self.ps_id, resp)
        error(custom_msg.OperationFailed())
    end
    return resp
end

function psu_manager:get_psu_register_info(cmd, length)
    local ok, resp = pcall(function ()
        return self.Power.methods.GetPsuRegisterInfo(cmd, length)
    end)
    if not ok then
        log:error('get ps%s register info failed, error: %s', self.ps_id, resp)
        error(custom_msg.OperationFailed())
    end
    return resp
end

function psu_manager:get(prop, ...)
    local ok, resp = pcall(self.psu_slot.get, self.psu_slot, prop, ...)
    if not ok then
        return nil, string.format('get ps%u %s failed: %s', self.ps_id, prop, resp)
    end
    return resp, E_OK
end

function psu_manager:is_healthy()
    return self.Health == psu_def.IS_HEALTH and self.OutputState ~= 0 and self.AlarmStatus & INPUT_ERROR_BIT == 0
end

function psu_manager:power_upgrade(upgrade_path, upgrade_process, para_tab)
    local ok, resp = pcall(function()
        return self.Upgrade.methods.Upgrade(upgrade_path, upgrade_process, para_tab)
    end)
    self:next_tick(function()
        self:update_firmware_info()
    end)
    -- 电源升级完成后刷新一次电子标签
    self:next_tick(function()
        local config_value = self.system_power_instance:get_psu_fru_config()
        self.Power.methods.GetFruData(config_value)
    end)
    if ok then
        return resp
    end
    log:error('ps%s upgrade failed, resp = %s', self.ps_id, resp)
    return fw_def.SUPPLY_1
end

function psu_manager:psm_monitor_stop()
    local ok, resp = pcall(function()
        return self.Power.methods.PowerAccessControl(false)
    end)
    if not ok then
        log:error('ps%d contorl stop failed, ret: %s', self.ps_id, resp)
        return resp
    end
    return E_OK
end

function psu_manager:psm_monitor_start()
    local ok, resp = pcall(function()
        return self.Power.methods.PowerAccessControl(true)
    end)
    if not ok then
        log:error('ps%d contorl start failed, ret: %s', self.ps_id, resp)
        return resp
    end
    return E_OK
end

function psu_manager:update_power_level()
    if self.PowerLevel ~= "N/A" then
        log:debug("Power level has been updated to %s", self.PowerLevel)
        return
    end
    for _ = 1, 10, 1 do
        if not self.position or not self.PartNumber then
            goto continue
        end
        c_efficiency_curve_object.collection:fold(function(_, obj)
            if obj.Position == self.position and obj.PartNumber == self.PartNumber and obj.PowerLevel
                and obj.PowerLevel ~= "N/A" then
                self.PowerLevel = obj.PowerLevel
            end
        end)
        if self.PowerLevel ~= "N/A" then
            break
        end
        ::continue::
        skynet.sleep(1000)
    end
    if self.PowerLevel == "N/A" then
        log:error('Update power[%s] level failed', self.position)
        return
    end
    log:notice('Update power[%s] level successfully, current level: %s', self.position, self.PowerLevel)
end

function psu_manager:update_efficiency_curve()
    local curve = false
    -- 读取10次，每次间隔十秒，如果100秒内读取不到电源信息，就退出
    for _ = 1, 10, 1 do
        if not self.position or not self.PartNumber or not self.InputVoltage or self.InputVoltage == 0 then
            goto continue
        end
        curve = c_efficiency_curve_object.collection:find(function(obj)
            return obj.Position == self.position and obj.PartNumber == self.PartNumber and
                math.abs(obj.InputVoltage - self.InputVoltage) < 10 
        end)
        if curve then
            break
        end
        ::continue::
        skynet.sleep(1000)
    end
    if curve and curve.PowerLevel ~= "N/A" then
        self.PowerLevel = curve.PowerLevel
        log:notice('Update power[%s] level successfully, current level: %s', self.position, self.PowerLevel)
    end
    if not curve or not curve.LoadPercentRange or not curve.EfficiencyCurve then
        log:error('Load power[%s] efficiency curve failed', self.position)
        return
    end
    if #curve.LoadPercentRange ~= #curve.EfficiencyCurve then
        log:error('Power[%s] efficiency is configured incorrectly', self.PartNumber)
        return
    end
    local prop_value = {}
    for i = 1, #curve.LoadPercentRange do
        table.insert(prop_value, {curve.LoadPercentRange[i], curve.EfficiencyCurve[i]})
    end
    -- 需要用修改后的数据显式地设置属性值，修改才会生效
    self.EfficiencyCurve = prop_value
end

function psu_manager:is_attribute_mismatch(attribute)
    local mismatch = false
    local attribute_value
    psu_manager.collection:fold(function (_, psu_obj)
        if not attribute_value then
            attribute_value = psu_obj[attribute]
        elseif psu_obj[attribute] ~= attribute_value then
            mismatch = true
        end
    end)
    return mismatch
end

function psu_manager:is_mismatch()
    local is_manufacturer_mismatch = self:is_attribute_mismatch("Manufacturer")
    local is_model_mismatch = self:is_attribute_mismatch("Model")
    log:notice("For power supplies, manufacturer mismatch: %s, model mismatch: %s",
        is_manufacturer_mismatch, is_model_mismatch)
    self.power_mgmt_instance:set_value("IsManufacturerMismatch", is_manufacturer_mismatch)
    self.power_mgmt_instance:set_value("IsModelMismatch", is_model_mismatch)
end

function psu_manager.create_mdb_object()
end

function psu_manager:update_firmware()
    log:notice('The firmware version of PSU%s changes from %s to %s',
        self.ps_id, self.last_firmware_version, self.FirmwareVersion)
    self:update_firmware_info()
    self.last_firmware_version = self.FirmwareVersion
end

function psu_manager:update_rate()
    if self.last_rate ~= DEFAULT then
        log:maintenance(log.MLOG_INFO, log.FC__PUBLIC_OK, 
            'The rate of PSU%s changes from %s to %s, and the input voltage changes from %s to %s',
            self.ps_id, self.last_rate, self.Rate, self.last_input_voltage, self.InputVoltage)
    end
    self.last_rate = self.Rate
    self.last_input_voltage = self.InputVoltage
end

function psu_manager:collect_black_box_data()
    local ok, uptime = pcall(utils_core.get_bmc_uptime)
    if not ok then
        log:error('Get bmc uptime failed')
        return
    end
    if self.last_collect_time ~= 0 and uptime - self.last_collect_time < 60 then
        log:error('Insuffcient time since the last collection, uptime: %s, last collect time: %s', uptime,
            self.last_collect_time)
        return
    end
    local bmc_time
    client:ForeachTimeObjects(function (obj)
        bmc_time = obj['DateTime']
    end)
    if not bmc_time then
        log:error('Get bmc time failed')
        return
    end
    local ret, black_box_max_length, black_box_data = self:get_black_box_data()
    if ret == E_FAILED then
        return
    end
    log:ps_black_box_print('PSU%s black box info:', self.ps_id)
    log:ps_black_box_print('BMC Time:%s    Power SN:%s', bmc_time, self.SerialNumber)
    for i = 1, black_box_max_length // log_def.BLACK_BOX_LINE_LEN do
        local str_tmp = black_box_data:sub(
            (i - 1) * log_def.BLACK_BOX_LINE_LEN + 1, i * log_def.BLACK_BOX_LINE_LEN)
        local str_len = #str_tmp
        log:ps_black_box_print(string.format('0x%03x: ' .. string.rep('0x%02x ', str_len), i - 1,
            string.unpack(string.rep('B', str_len), str_tmp)) ..
            string.rep('0x00 ', log_def.BLACK_BOX_LINE_LEN - str_len))
    end
    self.last_collect_time = uptime
end

function psu_manager:tpsu_status_high_changed_callback()
    -- 当电源故障状态存在bit位从0变1时，重新进行黑匣子收集
    local new_ps_status = self.TPSUStatusHigh ~ self.last_tspu_status_high
    if (self.TPSUStatusHigh & new_ps_status) ~= 0 then
        self:next_tick(function()
            self:collect_black_box_data()
        end)
    end
    log:notice('The tpsu status high of psu%s changes from %s to %s', self.ps_id,
        self.last_tspu_status_high, self.TPSUStatusHigh)
    self.last_tspu_status_high = self.TPSUStatusHigh
end

function psu_manager:tpsu_status_low_changed_callback()
    -- 当电源故障状态存在bit位从0变1时，重新进行黑匣子收集
    local new_ps_status = self.TPSUStatusLow ~ self.last_tspu_status_low
    if (self.TPSUStatusLow & new_ps_status) ~= 0 then
        self:next_tick(function()
            self:collect_black_box_data()
        end)
    end
    log:notice('The tpsu status low of psu%s changes from %s to %s', self.ps_id,
        self.last_tspu_status_low, self.TPSUStatusLow)
    self.last_tspu_status_low = self.TPSUStatusLow
end

function psu_manager:prop_listening_register()
    self:listen('CommunicationStatus', function ()
        log:notice('The commuinication status of PSU%s changes from %s to %s', self.ps_id,
            self.last_comm_status, self.CommunicationStatus)
        if self.last_comm_status == 1 and self.CommunicationStatus == 0 then
            self.Power.methods.UpdatePowerSupplyInfo()
            self:update_firmware_info()
            local config_value = self.system_power_instance:get_psu_fru_config()
            self.Power.methods.GetFruData(config_value)
        end
        self.last_comm_status = self.CommunicationStatus
    end)
    self:listen('FirmwareVersion', function() self:update_firmware() end)
    self:listen('Rate', function() self:update_rate() end)
    self:listen('TPSUStatusHigh', function()
        self:tpsu_status_high_changed_callback()
    end)
    self:listen('TPSUStatusLow', function()
        self:tpsu_status_low_changed_callback()
     end)
end

function psu_manager:init()
    self.ps_id = self.SlotNumber
    self.last_comm_status = self.CommunicationStatus
    self.last_tspu_status_high = self.TPSUStatusHigh
    self.last_tspu_status_low = self.TPSUStatusLow
    self:connect_signal(self.on_add_object_complete, function()
        self:next_tick(function()
            self:register_fw_info()
            self:prop_listening_register()
        end)
        -- 检测到OnePower对象分发时要刷新一次电子标签
        self:next_tick(function()
            local config_value = self.system_power_instance:get_psu_fru_config()
            self.Power.methods.GetFruData(config_value)
        end)
        -- 添加电源转换效率曲线
        self:next_tick(function()
            self:update_efficiency_curve()
            self:update_power_level()
        end)
        -- 电源是否一致匹配
        self:next_tick(function() self:is_mismatch() end)
    end)
    psu_manager.super.init(self)
end

return psu_manager